iT邦幫忙

2023 iThome 鐵人賽

DAY 6
1

有了 Unicode 這本字典之後,我們很容易的就可以將字符和對應的碼位關聯在一起。

舉例來說,你 這個字的碼位為 U+4F60,也就是被編列在 0x4F60 的格子,其中 0x 表示十六進位,轉成十進位的話是第 20320 個格子。

那麼,接下來的問題是,電腦要怎麼儲存?因為電腦只認得 0 與 1 啊。

在前天 DAY 04 提到的 ASCII 裡面,每個字符都固定為 1 個 Byte,也就是 8 個 bit,共有 256 種組合。換句話說,只要我們想要放入的字符數量少於 256 個,就可以透過 1 個 Byte 來表示形成一對一的對應關係。

但是在 Unicode 裡,可是規劃了 1,114,112 個格子呀!我們要怎麼把對應的碼位「轉變成」電腦看得懂的形式呢?於是,Unicode 定義了不同的 UTF 格式(Unicode Transformation Format),用來將碼位轉為二進位形式,其中包含 UTF-8、UTF-16 和 UTF-32 三種實現方法。

UTF-32

UTF-32 顧名思義,就是將每個字都用 4 個 Byte,總共 32 個 bit 來進行碼位的編碼。考慮到每個 bit 都可以是 0 或 1,因此至多可以產生 2^32 = 4,294,967,296 個組合,這個數量已經遠遠超出 Unicode 的所有格子,因此,我們只要很簡單的把 Unicode 的碼位轉成二進位後,然後把前面補零即可。

以海豚 🐬 為例,其在 Unicode 的編碼為 U+1F42C,轉成二進位後是 0001 1111 0100 0010 1100,往前補齊零就是 UTF-32 的格式。

Byte Byte Byte Byte
十六進位 1 F4 2C
二進位 0001 1111 0100 0010 1100
UTF-32 0000 0000 0000 0001 1111 0100 0010 1100

UTF-16

不過,如果你仔細觀察,會發現 UTF-32 的前 15 個 bit(第一個 Byte 全部、以及第二個 Byte 的前七個 bit)始終都為零,畢竟 Unicode 的碼位數目用不到那麼多嘛,所以用 UTF-32 格式來儲存一個字符,其實是件相對沒效率的事情。

於是,Unicode 另外訂定了使用 16 個 bit,也就是 2 個 Byte 的 UTF-16 格式。

但是如此一來,便只剩下 2^16 = 65,536 個組合了,對於第 0 基本平面(BMP)的字符來說,這個數目剛剛好,但若要顯示第 1 到第 16 輔助平面的字符就完全不夠用了!

該怎麼辦?因此 UTF-16 用了相對動態的方式來儲存字符——如果該字符在 BMP 上,就用 2 個 Byte 來紀錄碼位;但如果該字符在輔助平面上,則和 UTF-32 一樣,使用 4 個 Byte 來儲存。

在 BMP 上,Unicode 預留了 U+D800 到 U+DFFF 為代理對(surrogate pair),其中 U+D800 到 U+DBFF 為高位代理區(high surrogates)、 U+DC00 到 U+DFFF 為低位代理區(low surrogates),兩區各有 1024 個碼位,這兩區的碼位不做任何的字符分配。

需要「代理」的時候,只要將兩者各取一碼,則能產生 1024*1024 = 1,048,576 種組合——剛好對應第 1 到第 16 輔助平面上的 1,048,576 個字符!

以剛剛的海豚 🐬 為例,其碼位以十六進位表示為 0x1F42C:

  1. 我們先把它減去 0x10000 ,得到 0x0F42C
  2. 將 0x0F42C 轉成二進位後是 0000 1111 0100 0010 1100
  3. 接著把這串二進位 對半切,拆成高位與低位的兩組,即 0000 1111 01 與 00 0010 1100
  4. 再將兩組二進位轉成十六進位,分別是 0x3D 與 0x2C。
  5. 前者(高位)加上 0xD800,得到 0xD83D
  6. 後者(低位)加上 0xDC00,得到 0xDC2C
  7. 因此,0x1F42C 便可以用 D8 3D DC 2C 來表示。

而對於程式處理來說,平常都預設使用兩個兩個 Byte 為一組去讀取字符,但只要掃到高位代理區的碼位,就可以知道接下來一定是低位代理區的碼位,且該「字」是由四個 Byte 來儲存的。

如此一來,我們就可以用一種相對有彈性的方法來處理 Unicode 了。

UTF-8

儘管 UTF-16 允許使用至多四個 Byte——常用的 BMP 只要兩個 Byte 就足夠——來紀錄 Unicode,但我們還能不能用更少的 Byte?舉例來說,英文、數字以及大部分的符號都放前 128 個位置,這裡只需要用 8 個 bit、一個 Byte 來表示就足夠了!

UTF-8 讓我們得以依循古法,將 ASCII(0x00 - 0x7F)的字符用 1 個 Byte 來表示,0x80 - 0x7FF 使用 2 個 Byte,0x800 - 0xFFFF 用 3 個 Byte,輔助平面的 0x10000 - 0x10FFFF 則用 4 個 Byte 來儲存。計算方法也相對簡單,只要將碼位轉成二進位再按表填入即可,剩餘的高位補零:

碼位 Byte Byte Byte Byte
0x00 - 0x7F 0xxxxxxx
0x80 - 0x7FF 110xxxxx 10xxxxxx
0x800 - 0xFFFF 1110xxxx 10xxxxxx 10xxxxxx
0x10000 - 0x10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

一樣再舉海豚 🐬 為例,0x1F42C 轉成 00011111010000101100,由右至左填入後可得

Byte Byte Byte Byte
11110000 10011111 10010000 10101100
F0 9F 90 AC

還記得我們前面有提到,ASCII 會成功的關鍵之一,便是因為其保留最高位的 bit 為 0,讓後來擴充字集都可以很簡單的「迴避」基本區(只要不用 0 開頭就好)。這點在 UTF-8 編碼規則上尤其重要,程式只要看到非 1 開頭的 Byte,就可以知道這個「字」僅佔了 1 個 Byte;而剩餘以 1 開頭的字符,則只要知道有幾個 1 ,就可以知道該字的長度——以 1110 為開頭的話,便可以知道該字符佔了 3 個 Byte。

對於一份僅有英數的文件和檔案來說,使用 UTF-8 編碼存擋,僅需要 UTF-32 的四分之一、UTF-16 的一半大小。

UTF-8 已經是目前多數網頁和文件的事實(de facto)編碼標準。

遺留的技術債

UTF-8 相較於其他兩種編碼,雖然其字符長度是可變的(有可能 1 Byte 也有可能 3 Byte),但也相對地節省空間。

考量到平常會用到的字都在 BMP 上,不太有需要使用到輔助平面,因此 MySQL 預設只使用了至多 3 個 Byte 來儲存字符,這個規格叫做 utf8mb3,其中的 mb 表示 most byte,也就是至多的 Byte 數量。

但在 Emoji 出現之後,utf8mb3 就無法正確的儲存這些小可愛了,所以後來 MySQL 才新增了可以儲存四位 Byte 的 utf8mb4 格式。如果你的 Wordpress 無法儲存 Emoji 的話,通常就是因為使用了舊的 utf8mb3 格式,趕快把他調成 utf8mb4 吧。

Recap

每個字使用的 Byte 優點 缺點
ASCII 1 Byte 最簡單。如果只要顯示英文、數字與符號已經足夠 超出範圍的字符將無法編碼
UTF-32 4 Byte 無需轉換,可以與 Unicode 碼位直接對應。字符固定為四個 Byte,程式處理方便 所佔空間大,會有大量的 bit 為 0
UTF-16 BMP 上的字符為 2 Byte,輔助平面的字符為 4 Byte 常用的字符都在 BMP 上,所以幾乎都能用 2 個 Byte 來表示 但 Emoji 在輔助平面上....
UTF-8 1~4 Byte 靈活,可以向下兼容 ASCII,最省空間 每個字符的 Byte 長度不一,程式要額外考慮「斷字」的切割

上一篇
DAY 05|Unicode 的統一與原則
下一篇
DAY 07|Unicode 收錄的漢字狀況
系列文
一起成為新世紀文字藝術師:深入玩轉 Unicode 和 OpenType 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
_cccheng
iT邦新手 5 級 ‧ 2023-09-22 10:13:10

UTF-32 補零的表十六進位是歪的?

chingru iT邦新手 4 級 ‧ 2023-09-22 14:53:13 檢舉

表格對齊歪了QQ 已修正 謝謝糾錯!

我要留言

立即登入留言